package com.immediatus.graphics.camera;

import android.util.FloatMath;
import com.immediatus.contracts.IUpdateHandler;
import com.immediatus.engine.collision.RectangularCollisionChecker;
import com.immediatus.engine.contracts.IUnit;
import com.immediatus.engine.input.TouchEvent;
import com.immediatus.engine.primitive.Line;
import com.immediatus.engine.shape.RectangularShape;
import com.immediatus.graphics.util.OpenGLWrapper;

import javax.microedition.khronos.opengles.GL10;

public class Camera implements IUpdateHandler{
    protected static final int VERTEX_INDEX_X = 0;
    protected static final int VERTEX_INDEX_Y = 1;

    protected static final float[] VERTICES_TOUCH_BUFFER = new float[2];

    private static final float DEG_TO_RAD = (float) (Math.PI) / 180.0f;

    private float _minX;
    private float _maxX;
    private float _minY;
    private float _maxY;

    private float _nearZ = -1.0f;
    private float _farZ = 1.0f;

    private IUnit _chaseUnit;

    protected float _rotation = 0;
    protected float _cameraRotation = 0;

    protected int _canvasX;
    protected int _canvasY;
    protected int _canvasWidth;
    protected int _canvasHeight;

    public Camera(final float x_, final float y_, final float w_, final float h_){
        this._minX = x_;
        this._maxX = x_ + w_;
        this._minY = y_;
        this._maxY = y_ + h_;
    }

    public float getMinX(){
        return this._minX;
    }

    public float getMaxX(){
        return this._maxX;
    }

    public float getMinY(){
        return this._minY;
    }

    public float getMaxY(){
        return this._maxY;
    }

    public float getNearZClippingPlane(){
        return this._nearZ;
    }

    public float getFarZClippingPlane(){
        return this._farZ;
    }

    public void setNearZClippingPlane(final float nearZClippingPlane_){
        this._nearZ = nearZClippingPlane_;
    }

    public void setFarZClippingPlane(final float farZClippingPlane_){
        this._farZ = farZClippingPlane_;
    }

    public void setZClippingPlanes(final float nearZClippingPlane_, final float farZClippingPlane_){
        this._nearZ = nearZClippingPlane_;
        this._farZ = farZClippingPlane_;
    }

    public float getWidth(){
        return this._maxX - this._minX;
    }

    public float getHeight(){
        return this._maxY - this._minY;
    }

    public float getWidthRaw(){
        return this._maxX - this._minX;
    }

    public float getHeightRaw(){
        return this._maxY - this._minY;
    }

    public float getCenterX(){
        final float minX = this._minX;
        return minX + (this._maxX - minX) * 0.5f;
    }

    public float getCenterY(){
        final float minY = this._minY;
        return minY + (this._maxY - minY) * 0.5f;
    }

    public void setCenter(final float x_, final float y_){
        final float dX = x_ - this.getCenterX();
        final float dY = y_ - this.getCenterY();

        this._minX += dX;
        this._maxX += dX;
        this._minY += dY;
        this._maxY += dY;
    }

    public void offsetCenter(final float x_, final float y_){
        this.setCenter(this.getCenterX() + x_, this.getCenterY() + y_);
    }

    public void setChaseUnit(final IUnit unit_){
        this._chaseUnit = unit_;
    }

    public float getRotation(){
        return this._rotation;
    }

    public void setRotation(final float rotation_){
        this._rotation = rotation_;
    }

    public float getCameraRotation(){
        return this._cameraRotation;
    }

    public void setCameraRotation(final float rotation_){
        this._cameraRotation = rotation_;
    }

    public int getCanvasX(){
        return this._canvasX;
    }

    public int getCanvasY(){
        return this._canvasY;
    }

    public int getCanvasWidth(){
        return this._canvasWidth;
    }

    public int getCanvasHeight(){
        return this._canvasHeight;
    }

    public void setSurfaceSize(final int x_, final int y_, final int w_, final int h_){
        this._canvasX = x_;
        this._canvasY = y_;
        this._canvasWidth = w_;
        this._canvasHeight = h_;
    }

    public boolean isRotated(){
        return this._rotation != 0;
    }

    @Override
    public void onUpdate(final float secondsElapsed_){
        this.updateChaseEntity();
    }

    @Override
    public void reset(){
    }

    public void updateChaseEntity(){
        if (this._chaseUnit != null){
            final float[] centerCoordinates = this._chaseUnit.getLayerCenterCoordinates();
            this.setCenter(centerCoordinates[VERTEX_INDEX_X], centerCoordinates[VERTEX_INDEX_Y]);
        }
    }

    public boolean isLineVisible(final Line line_){
        return RectangularCollisionChecker.isVisible(this, line_);
    }

    public boolean isRectangularShapeVisible(final RectangularShape shape_){
        return RectangularCollisionChecker.isVisible(this, shape_);
    }

    public void onApplyLayerMatrix(final GL10 gl_){
        OpenGLWrapper.setProjectionIdentityMatrix(gl_);

        gl_.glOrthof(this.getMinX(), this.getMaxX(), this.getMaxY(), this.getMinY(), this._nearZ, this._farZ);

        final float rotation = this._rotation;
        if (rotation != 0){
            this.applyRotation(gl_, this.getCenterX(), this.getCenterY(), rotation);
        }
    }

    public void onApplyLayerBackgroundMatrix(final GL10 gl_){
        OpenGLWrapper.setProjectionIdentityMatrix(gl_);

        final float widthRaw = this.getWidthRaw();
        final float heightRaw = this.getHeightRaw();

        gl_.glOrthof(0, widthRaw, heightRaw, 0, this._nearZ, this._farZ);

        final float rotation = this._rotation;
        if (rotation != 0){
            this.applyRotation(gl_, widthRaw * 0.5f, heightRaw * 0.5f, rotation);
        }
    }

    public void onApplyCameraLayerMatrix(final GL10 gl_){
        OpenGLWrapper.setProjectionIdentityMatrix(gl_);

        final float widthRaw = this.getWidthRaw();
        final float heightRaw = this.getHeightRaw();

        gl_.glOrthof(0, widthRaw, heightRaw, 0, this._nearZ, this._farZ);

        final float cameraSceneRotation = this._cameraRotation;
        if (cameraSceneRotation != 0){
            this.applyRotation(gl_, widthRaw * 0.5f, heightRaw * 0.5f, cameraSceneRotation);
        }
    }

    private void applyRotation(final GL10 gl_, final float x_, final float y_, final float angle_){
        gl_.glTranslatef(x_, y_, 0);
        gl_.glRotatef(angle_, 0, 0, 1);
        gl_.glTranslatef(-x_, -y_, 0);
    }

    public void convertLayerToCameraTouchEvent(final TouchEvent touchEvent_){
        this.unapplyLayerRotation(touchEvent_);
        this.applyLayerToCameraOffset(touchEvent_);
        this.applyCameraRotation(touchEvent_);
    }

    public void convertCameraToLayerTouchEvent(final TouchEvent touchEvent_){
        this.unapplyCameraRotation(touchEvent_);
        this.unapplyLayerToCameraOffset(touchEvent_);
        this.applyLayerRotation(touchEvent_);
    }

    protected void applyLayerToCameraOffset(final TouchEvent touchEvent_){
        touchEvent_.offset(-this._minX, -this._minY);
    }

    protected void unapplyLayerToCameraOffset(final TouchEvent touchEvent_){
        touchEvent_.offset(this._minX, this._minY);
    }

    private void applyLayerRotation(final TouchEvent touchEvent_){
        final float rotation = -this._rotation;
        if (rotation != 0){
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X] = touchEvent_.getX();
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y] = touchEvent_.getY();

            rotateAroundCenter(VERTICES_TOUCH_BUFFER, rotation, this.getCenterX(), this.getCenterY());

            touchEvent_.set(VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X], VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y]);
        }
    }

    private void unapplyLayerRotation(final TouchEvent touchEvent_){
        final float rotation = this._rotation;

        if (rotation != 0){
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X] = touchEvent_.getX();
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y] = touchEvent_.getY();

            revertRotateAroundCenter(VERTICES_TOUCH_BUFFER, rotation, this.getCenterX(), this.getCenterY());

            touchEvent_.set(VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X], VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y]);
        }
    }

    private void applyCameraRotation(final TouchEvent touchEvent_){
        final float cameraSceneRotation = -this._cameraRotation;

        if (cameraSceneRotation != 0){
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X] = touchEvent_.getX();
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y] = touchEvent_.getY();

            rotateAroundCenter(VERTICES_TOUCH_BUFFER, cameraSceneRotation,
                    (this._maxX - this._minX) * 0.5f, (this._maxY - this._minY) * 0.5f);

            touchEvent_.set(VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X], VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y]);
        }
    }

    private void unapplyCameraRotation(final TouchEvent touchEvent_){
        final float cameraSceneRotation = -this._cameraRotation;

        if (cameraSceneRotation != 0){
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X] = touchEvent_.getX();
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y] = touchEvent_.getY();

            revertRotateAroundCenter(VERTICES_TOUCH_BUFFER, cameraSceneRotation,
                    (this._maxX - this._minX) * 0.5f, (this._maxY - this._minY) * 0.5f);

            touchEvent_.set(VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X], VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y]);
        }
    }

    public void convertSurfaceToLayerTouchEvent(final TouchEvent touchEvent_, final int w_, final int h_){
        final float relativeX;
        final float relativeY;

        final float rotation = this._rotation;
        if (rotation == 0){
            relativeX = touchEvent_.getX() / w_;
            relativeY = touchEvent_.getY() / h_;
        } else if (rotation == 180){
            relativeX = 1 - (touchEvent_.getX() / w_);
            relativeY = 1 - (touchEvent_.getY() / h_);
        } else{
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X] = touchEvent_.getX();
            VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y] = touchEvent_.getY();

            rotateAroundCenter(VERTICES_TOUCH_BUFFER, rotation, w_ / 2, h_ / 2);

            relativeX = VERTICES_TOUCH_BUFFER[VERTEX_INDEX_X] / w_;
            relativeY = VERTICES_TOUCH_BUFFER[VERTEX_INDEX_Y] / h_;
        }

        this.convertAxisAlignedSurfaceToLayerTouchEvent(touchEvent_, relativeX, relativeY);
    }

    private void convertAxisAlignedSurfaceToLayerTouchEvent(final TouchEvent touchEvent_, final float x_, final float y_){
        final float minX = this.getMinX();
        final float maxX = this.getMaxX();
        final float minY = this.getMinY();
        final float maxY = this.getMaxY();

        final float x = minX + x_ * (maxX - minX);
        final float y = minY + y_ * (maxY - minY);

        touchEvent_.set(x, y);
    }

    protected static float[] rotateAroundCenter(final float[] vertices_, final float rotation_, final float rotationCenterX_, final float rotationCenterY_){
        if (rotation_ != 0){
            final float rotationRad = DEG_TO_RAD * rotation_;
            final float sinRotationRad = FloatMath.sin(rotationRad);
            final float cosRotationInRad = FloatMath.cos(rotationRad);

            for (int i = vertices_.length - 2; i >= 0; i -= 2){
                final float pX = vertices_[i];
                final float pY = vertices_[i + 1];
                vertices_[i] = rotationCenterX_ +
                        (cosRotationInRad * (pX - rotationCenterX_) - sinRotationRad * (pY - rotationCenterY_));
                vertices_[i + 1] = rotationCenterY_ +
                        (sinRotationRad * (pX - rotationCenterX_) + cosRotationInRad * (pY - rotationCenterY_));
            }
        }
        return vertices_;
    }

    protected static float[] revertRotateAroundCenter(final float[] vertices_, final float rotation_, final float rotationCenterX_, final float rotationCenterY_){
        return rotateAroundCenter(vertices_, -rotation_, rotationCenterX_, rotationCenterY_);
    }

    protected static float[] scaleAroundCenter(final float[] vertices_, final float scaleX_, final float scaleY_, final float scaleCenterX_, final float scaleCenterY_){
        if (scaleX_ != 1 || scaleY_ != 1){
            for (int i = vertices_.length - 2; i >= 0; i -= 2){
                vertices_[i] = scaleCenterX_ + (vertices_[i] - scaleCenterX_) * scaleX_;
                vertices_[i + 1] = scaleCenterY_ + (vertices_[i + 1] - scaleCenterY_) * scaleY_;
            }
        }

        return vertices_;
    }

    protected static float[] revertScaleAroundCenter(final float[] vertices_, final float scaleX_, final float scaleY_, final float scaleCenterX_, final float scaleCenterY_){
        return scaleAroundCenter(vertices_, 1 / scaleX_, 1 / scaleY_, scaleCenterX_, scaleCenterY_);
    }
}
